package de.bloxel.core;

import static com.google.common.base.Preconditions.checkNotNull;
import static de.bloxel.core.BloxelFactory.BloxelType.GLASS;
import static de.bloxel.core.BloxelFactory.BloxelType.GRASS;
import static de.bloxel.core.BloxelFactory.BloxelType.POND;
import static de.bloxel.core.BloxelFactory.BloxelType.WATER;

import java.util.HashMap;
import java.util.Map;

import com.jme3.asset.AssetManager;
import com.jme3.bullet.collision.shapes.BoxCollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.shape.Box;
import com.jme3.water.SimpleWaterProcessor;

/**
 * This factory create the {@link Bloxel}S of our Bloxel-World. Even if we place more complex objects in the world, this
 * Factory should be used to create the necessary {@link Bloxel}S of this objects, i.e a Bloxel-Tree have many
 * (different) {@link Bloxel}S.
 * 
 * Materials: http://jmonkeyengine.org/wiki/doku.php/jme3:advanced:materials_overview
 * 
 * TODO use builder pattern to create the available bloxel in better way
 * 
 * @author andreas
 * @since 0.1.0
 */
public class BloxelFactory {

  public enum BloxelType {
    GRASS, GLASS, POND, WATER
  }

  public static float DEFAULT_BLOXEL_DIMENSION = 0.5f;

  private static final BoxCollisionShape BOX_COLLISION_SHAPE = new BoxCollisionShape(new Vector3f(
      DEFAULT_BLOXEL_DIMENSION, DEFAULT_BLOXEL_DIMENSION, DEFAULT_BLOXEL_DIMENSION));

  private final Map<BloxelType, Material> materials = new HashMap<BloxelType, Material>();
  private final Map<BloxelType, Mesh> meshes = new HashMap<BloxelType, Mesh>();

  private final SimpleWaterProcessor waterProcessor;

  /**
   * @param theAssetManager
   *          must not be {@code null}
   */
  public BloxelFactory(final AssetManager theAssetManager) {
    this(theAssetManager, null);
  }

  /**
   * @param theAssetManager
   *          must not be {@code null}, use for texture/material creation
   * @param theWaterProcessor
   *          optional, used for better water-surfaces
   */
  public BloxelFactory(final AssetManager theAssetManager, final SimpleWaterProcessor theWaterProcessor) {
    waterProcessor = theWaterProcessor;
    loadMaterials(checkNotNull(theAssetManager));
    loadMeshes();
  }

  /**
   * @param type
   *          to use
   * 
   * @return a {@link Bloxel} for the given {@link BloxelType type}, the {@link Bloxel} use the default {@link Box
   *         box-mesh}, use {@link #create(Mesh, BloxelType)} to create a other kind of {@link Bloxel}
   */
  public Bloxel create(final BloxelType type) {
    final Bloxel bloxel = new Bloxel("Bloxel", meshes.get(type));
    setMaterial(bloxel, type);
    if (type.equals(GLASS)) {
      bloxel.setQueueBucket(Bucket.Transparent);
    } else {
      bloxel.setQueueBucket(Bucket.Opaque);
    }
    bloxel.addControl(new RigidBodyControl(BOX_COLLISION_SHAPE, 0));
    bloxel.getControl(RigidBodyControl.class).setKinematic(true);
    bloxel.getControl(RigidBodyControl.class).setFriction(10f);
    return bloxel;
  }

  /**
   * @param aMesh
   *          would be used to create a {@link Geometry}, must not be {@code null}
   * @param aType
   *          to use for creating
   * 
   * @return a {@link Bloxel} for the given {@link BloxelType type}, the {@link Bloxel} use the given {@link Mesh mesh}
   * @deprecated not longer used, will be removed
   */
  @Deprecated
  public Bloxel create(final Mesh aMesh, final BloxelType aType) {
    final Bloxel bloxel = new Bloxel("Bloxel", checkNotNull(aMesh));
    setMaterial(bloxel, aType);
    return bloxel;
  }

  /**
   * @param theAssetManager
   *          must not be {@code null}
   * @return
   */
  private Material getLightingMaterial(final AssetManager theAssetManager) {
    return new Material(checkNotNull(theAssetManager), "Common/MatDefs/Light/Lighting.j3md");
  }

  /**
   * @param theAssetManager
   *          must not be {@code null}
   * @return
   */
  private Material getSimpleTexturedMaterial(final AssetManager theAssetManager) {
    return new Material(checkNotNull(theAssetManager), "Common/MatDefs/Misc/SimpleTextured.j3md");
  }

  /**
   * A translucent/transparent texture, similar to a window frame.
   * 
   * @param theAssetManager
   *          must not be {@code null}
   */
  private void loadMaterialGlass(final AssetManager theAssetManager) {
    final Material material = getLightingMaterial(checkNotNull(theAssetManager));
    material.setTexture("DiffuseMap", theAssetManager.loadTexture("Textures/Bloxels/glass.png"));
    material.setTexture("NormalMap", theAssetManager.loadTexture("Textures/Bloxels/glass_normal.png"));
    // activate transparency
    material.getAdditionalRenderState().setBlendMode(BlendMode.Alpha);
    materials.put(GLASS, material);
  }

  /**
   * Load simple grass texture.
   * 
   * @param theAssetManager
   *          must not be {@code null}
   */
  private void loadMaterialGrass(final AssetManager theAssetManager) {
    final Material material = getLightingMaterial(checkNotNull(theAssetManager));
    material.setTexture("DiffuseMap", theAssetManager.loadTexture("Textures/Terrain/splat/grass.jpg"));
    material.setTexture("NormalMap", theAssetManager.loadTexture("Textures/Terrain/splat/grass_normal.png"));
    material.setFloat("Shininess", 1.5f); // [0,128]
    materials.put(GRASS, material);
  }

  /**
   * @param theAssetManager
   *          must not be {@code null}
   */
  private void loadMaterialPond(final AssetManager theAssetManager) {
    final Material material = getLightingMaterial(checkNotNull(theAssetManager));
    material.setTexture("DiffuseMap", theAssetManager.loadTexture("Textures/Terrain/Pond/Pond.png"));
    material.setTexture("NormalMap", theAssetManager.loadTexture("Textures/Terrain/Pond/Pond_normal.png"));
    material.setFloat("Shininess", 1.5f); // [0,128]
    materials.put(POND, material);
  }

  private void loadMaterials(final AssetManager theAssetManager) {
    loadMaterialGrass(theAssetManager);
    loadMaterialGlass(theAssetManager);
    loadMaterialPond(theAssetManager);
    loadMaterialWater(theAssetManager);
  }

  private void loadMaterialWater(final AssetManager theAssetManager) {
    if (waterProcessor == null) {
      final Material mat = new Material(theAssetManager, "Common/MatDefs/Misc/SolidColor.j3md");
      mat.setColor("Color", ColorRGBA.Blue);
      materials.put(WATER, mat);
    } else {
      final Material material = waterProcessor.getMaterial();
      // material.setColor("waterColor", ColorRGBA.Blue);
      // material.setFloat("waterDepth", waterDepth);
      // material.setFloat("waterTransparency", 0.4f);
      materials.put(WATER, material);
    }
  }

  private void loadMeshes() {
    // die pond texture muss skaliert werden, drum muss ich hier ein eigenes mesh laden - denke ich ?!
    final Mesh pondBox = new Box(DEFAULT_BLOXEL_DIMENSION, DEFAULT_BLOXEL_DIMENSION, DEFAULT_BLOXEL_DIMENSION);
    pondBox.scaleTextureCoordinates(new Vector2f(1f, 1f));
    meshes.put(POND, pondBox);
    // sonst eine "ganz" normale box
    final Mesh defaultBox = new Box(Vector3f.ZERO, DEFAULT_BLOXEL_DIMENSION, DEFAULT_BLOXEL_DIMENSION,
        DEFAULT_BLOXEL_DIMENSION);
    meshes.put(GRASS, defaultBox);
    meshes.put(GLASS, defaultBox);
    meshes.put(WATER, defaultBox);
  }

  private void setMaterial(final Bloxel bloxel, final BloxelType type) {
    bloxel.setMaterial(checkNotNull(materials.get(type)));
  }
}
